﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Eventless
{
    public sealed class Computed<T> : Forwarder<Writeable<T>, T>, IReadable<T>, IDisposable
    {
        private readonly Func<T> _compute;

        private ISet<IReadable> _subscriptions = Computed.EmptySubscriptions;

        public Computed(Func<T> compute)
            : base(new Writeable<T>())
        {
            _compute = compute;
            Recompute();
        }

        private void Recompute()
        {
            var newSubscriptions = new HashSet<IReadable>();

            Computations.Push(o => newSubscriptions.Add(o));
            var newVal = _compute();
            Computations.Pop();
            Impl.Value = newVal;
            newSubscriptions.Remove(this);
            newSubscriptions.Remove(Impl);

            foreach (var sub in _subscriptions.Where(s => !newSubscriptions.Contains(s)))
                sub.Changed -= Recompute;
        
            foreach (var sub in newSubscriptions.Where(s => !_subscriptions.Contains(s)))
                sub.Changed += Recompute;
        
            _subscriptions = newSubscriptions;
        }

        public T Value
        {
            get { return Impl.Value; }
        }

        public void Dispose()
        {
            foreach (var sub in _subscriptions)
                sub.Changed -= Recompute;
        }
    }

    public static class Computed
    {
        internal static readonly ISet<IReadable> EmptySubscriptions = new HashSet<IReadable>();

        public static void Do(Action compute)
        {
            new Computed<int>(() =>
                {
                    compute();
                    return 0;
                });
        }

        public static IReadable<T> From<T>(Func<T> compute)
        {
            return new Computed<T>(compute);
        }

        public static IWriteable<T> From<T>(Func<T> get, Action<T> set)
        {
            return new Mutated<T>(get, set);
        }

        public static IReadable<T> FromAsync<T>(this IReadable<Task<T>> asyncObs, T initValue = default(T))
        {
            var plainObservable = new Writeable<T>(initValue);
            var counter = 0;

            Do(() =>
                {
                    var invocation = ++counter;
                    var task = asyncObs.Value;
                    if (task == null) 
                        return;
                    if (task.IsCompleted)
                        plainObservable.Value = task.Result;
                    else
                    {
                        task.ContinueWith(t =>
                        {
                            try
                            {
                                var result = t.Result;
                                if (invocation == counter)
                                    plainObservable.Value = result;
                            }
                            catch (AggregateException)
                            {
                                if (invocation == counter)
                                    plainObservable.Value = initValue;
                            }
                        });
                    }
                });

            return plainObservable;
        }
    }
}